3. 트랜스포트 계층

@VERO
Created Date · 2023년 12월 14일 14:12
Last Updated Date · 2025년 04월 20일 19:04

트랜스포트 계층 서비스 및 개요

트랜스포트 계층 프로토콜은 각기 다른 호스트에서 동작하는 애플리케이션 프로세스 간의 논리적 통신을 제공한다. 논리적 통신은 애플리케이션의 관점에서 보면 프로세스들이 동작하는 호스트들이 직접 연결된 것처럼 보인다는 것을 의미한다.

트랜스포트 계층 프로토콜은 네트워크 라우터가 아닌 종단 시스템에서 구현된다. 송신 측의 트랜스포트 계층은 송신 애플리케이션 프로세스로부터 수신한 메시지를 트랜스포트 계층 세그먼트 라고 알려진 트랜스포트 계층 패킷으로 변환한다. 트랜스포트 계층은 송신 종단 시스템에 있는 네트워크 계층으로 세그먼트를 전달하고, 여기서 세그먼트가 네트워크 계층 패킷 안에 캡슐화되어 목적지로 전달된다.

네트워크 라우터는 오로지 데이터그램의 네트워크 계층 필드에 대해 동작한다는 것을 유념해야 한다. (즉, 라우터는 데이터그램 안에 캡슐화된 트랜스포트 계층 세그먼트의 필드를 검사하지 않는다.)

네트워크 애플리케이션에는 하나 이상의 트랜스포트 계층 프로토콜을 사용할 수 있다. 이러한 프로토콜은 서비스를 요청하는 애플리케이션에게 트랜스포트 계층에서 각기 다른 서비스 집합을 제공한다.

트랜스포트 계층과 네트워크 계층 사이의 관계

트랜스포트 계층 프로토콜은 각기 다른 호스트에서 동작하는 프로세스들 사이의 논리적 통신을 제공하지만, 네트워크 계층 프로토콜은 호스트들사이의 논리적 통신을 제공한다.

트랜스포트 계층 프로토콜들은 종단 시스템에 존재한다. 종단 시스템 안에서 트랜스포트 프로토콜은 애플리케이션 프로세스에서 네트워크 경계까지 메시지를 운반하며, 반대 방향으로 네트워크 계층에서 애플리케이션 프로세스로 메시지를 운반한다. 그러나 트랜스포트 프로토콜은 메시지가 네트워크 계층 내부에서 어떻게 이동하는지는 언급하지 않는다.

컴퓨터 네트워크는 애플리케이션에서 서로 다른 서비스를 제공하도록 하는 개별 프로토콜을 갖는 다양한 트랜스포트 프로토콜을 만들 수 있게 한다.

트랜스포트 계층이 제공할 수 있는 서비스는 하위 네트워크 계층 프로토콜의 서비스 모델에 의해 제약받는다. 네트워크 계층 프로토콜이 호스트 사이에서 전송되는 트랜스포트 계층 세그먼트에 대한 지연 보장이나 대역폭 보장을 제공할 수 없다면, 트랜스포트 계층 프로토콜은 프로세스끼리 전송하는 메시지에 대한 지연 보장이나 대역폭 보장을 제공할 수 없다.

하위 네트워크 프로토콜이 상응하는 서비스를 제공하지 못할 때도, 특정 서비스는 트랜스포트 프로토콜에 의해 제공될 수 있다. 트랜스포트 프로토콜은 네트워크 계층을 트랜스포트 계층 세그먼트의 기밀성을 보장할 수 없을 때도 침입자가 애플리케이션 메시지를 읽지 못하도록 암호화를 사용할 수 있다.

인터넷 트랜스포트 계층의 개요

인터넷은 애플리케이션 계층에게 두 가지 구별되는 트랜스포트 계층 프로토콜을 제공한다. 하나는 비신뢰적이고 비연결형인 서비스를 요청한 애플리케이션에게 제공하는 UDP(User Datagram Protocol) 이고, 다른 하나는 신뢰적이고 연결지향형 서비스를 요청한 애플리케이션에게 제공하는 TCP(Transmission Control Protocol) 다. 네트워크 애플리케이션을 설계할 때 애플리케이션 개발자는 이 두 가지 트랜스포트 프로토콜 중 하나를 명시해야 한다.

인터넷에서 트랜스포트 계층 패킷을 세그먼트라고 한다. 그러나 인터넷 문서에서는 TCP 에 대한 트랜스포트 계층 패킷을 세그먼트로 나타내면서, UDP에 대한 패킷을 데이터그램으로 표현하기도 한다.

인터넷의 네트워크 계층 프로토콜은 인터넷 프로토콜 (Internet Protocol, IP) 이라는 이름을 갖는다. IP 서비스 모델은 호스트들 간에 논리적 통신을 제공하는 최선형 전달 서비스이다. 이것은 IP 가 통신하는 호스트들 간에 세그먼트를 전달하기 위해 최대한 노력하지만, 어떠한 보장도 하지 않는 것을 의미한다. 특히, IP 는 세그먼트의 전달을 보장하지 않고 세그먼트가 순서대로 전달되는 것을 보장하지 않는다. 또한 IP 는 세그먼트 내부 데이터의 무결성을 보장하지 않는다. 이러한 이유로 IP 를 비신뢰적인 서비스 라고 부른다. 또한 모든 호스트가 적어도 하나의 IP 주소로 불리는 네트워크 계층 주소를 갖는다.

UDP와 TCP의 가장 기본적인 기능은 종단 시스템 사이의 IP 전달 서비스를 종단 시스템에서 동작하는 두 프로세스 간의 전달 서비스로 확장하는 것이다. 호스트 대 호스트 전달을 프로세스 대 프로세스 전달로 확장하는 것을 트랜스포트 계층 다중화역다중화라고 부른다. UDP와 TCP는 헤더에 오류 검출 필드를 포함함으로써 무결성 검사를 제공한다. 이러한 최소한의 두 가지 트랜스포트 계층 서비스 (프로세스 대 프로세스 데이터 전달과 오류 검출)가 UDP가 제공하는 유일한 두 가지 서비스다. 여기서 UDP는 IP와 마찬가지로 비신뢰적인 서비스다. UDP는 하나의 프로세스에 의해 전송된 데이터가 손상되지 않고 목적지 프로세스에 도착한다는 것을 보장하지 않는다.

반면, TCP는 애플리케이션에 몇 가지 추가적인 서비스를 제공한다. 가장 먼저 TCP는 신뢰적인 데이터 전송을 제공한다. 흐름 제어, 순서 번호, 확인 응답, 타이머를 사용함으로써 TCP는 송신하는 프로세스로부터 수신하는 프로세스에게 데이터가 순서대로 정확하게 전달되도록 확실하게 한다. 이처럼 TCP는 종단 시스템 간에 IP의 비신뢰적인 서비스를 프로세스 사이의 신뢰적인 데이터 전송 서비스로 만들어준다. 또한 TCP는 혼잡 제어를 사용한다. 혼잡 제어는 인터넷에 대한 통상적인 서비스처럼 야기한 애플리케이션에게 제공되는 특정 서비스가 아니라, 전체를 위한 일반 서비스다. 즉, 한 TCP 연결이 과도한 양의 트래픽으로 모든 통신하는 호스트들 사이의 스위치와 링크를 혼잡하게 하는 것을 방지하는 것이 TCP 혼잡 제어다. TCP는 혼잡한 네트워크 링크에서 각 TCP 연결이 링크의 대역폭을 공평하게 공유하여 통과하도록 해준다. 이것은 송신 측의 TCP가 네트워크에 보낼 수 있는 트래픽을 조절함으로써 수행된다. 반면, UDP 트래픽은 조절되지 않는다. UDP 트랜스포트 프로토콜을 사용하는 애플리케이션은 허용이 되는 한 그것이 만족하는 어떤 속도로든 전송할 수 있다.

다중화와 역다중화

네트워크 계층이 제공하는 호스트 대 호스트 전달 서비스에서 호스트에서 동작하는 애플리케이션에 대한 프로세스 대 프로세스 전달 서비스로 확장하는 것을 살펴본다.

목적지 호스트에서의 트랜스포트 계층은 바로 아래의 네트워크 계층으로부터 세그먼트를 수신한다. 트랜스포트 계층은 호스트에서 동작하는 해당 애플리케이션 프로세스에게 이 세그먼트의 데이터를 전달하는 의무를 진다. 컴퓨터의 트랜스포트 계층이 하위의 네트워크 계층으로부터 데이터를 수신할 때, 트랜스포트 계층은 프로세스 중 하나에게 수신한 데이터를 전달할 필요가 있다.

먼저, 네트워크 애플리케이션의 한 부분으로서 프로세스는 소켓을 갖고 있다. 이를 통해 네트워크에서 프로세스로 데이터를 전달하고, 또한 프로세스로부터 네트워크로 데이터를 전달하는 출입구 역할을 한다. 그러므로 수신 측 호스트의 트랜스포트 계층은 실제로 데이터를 직접 프로세스로 전달하지 않고, 중간 매개자인 소켓에게 전달한다. 어떤 주어진 시간에 수신 측 호스트에 하나 이상의 소켓이 있을 수 있으므로, 각각의 소켓은 하나의 유일한 식별자를 갖는다. 식별자의 포맷은 UDP, TCP 소켓인지에 따라 달라진다.

수신측 호스트가 수신한 트랜스포트 계층 세그먼트를 어떻게 적절한 소켓으로 향하게 하는지를 생각해보자. 각각의 트랜스포트 계층 세그먼트는 이러한 목적을 위해 세그먼트에 필드 집합을 갖고 있다. 수신 측의 트랜스포트 계층은 수신 소켓을 식별하기 위해 이러한 필드를 검사한다. 그리고 이 세그먼트를 해당 소켓으로 보낸다. 트랜스포트 계층 세그먼트의 데이터를 올바른 소켓으로 전달하는 작업을 역다중화(demultiplexing) 이라고 한다. 출발지 호스트에서 소켓으로부터 데이터를 모으고, 이에 대한 세그먼트를 생성하기 위해 각 데이터에 헤더 정보로 캡슐화하고, 그 세그먼트들을 네트워크 계층으로 전달하는 작업을 다중화(multiplexing) 이라고 한다.

transport-multiplexing.jpeg

중간 호스트의 트랜스포트 계층은 네트워크 계층 아래로부터 수신한 세그먼트를 위쪽의 프로세스 P1P_1 또는 P2P_2 로 반드시 역다중화해야 한다. 이는 도착한 세그먼트의 데이터가 이에 상응하는 프로세스의 소켓으로 전달되도록 해서 이루어진다.
또한 중간 호스트의 트랜스포트 계층은 프로세스의 소켓으로부터 외부로 나가는 데이터를 모으고, 다음에 트랜스포트 계층 세그먼트들로 만들고, 이 세그먼트들을 아래 네트워크 계층으로 전달해야만 한다.

트랜스포트 계층 다중화에는 다음 두 가지 요구사항이 있다.

  1. 소켓은 유일한 식별자를 갖는다.
  2. 각 세그먼트는 세그먼트가 전달될 적절한 소켓을 가리키는 특별한 필드를 갖는다.

이 특별한 필드라는 것은 출발지 포트 번호 필드 (source port number field)목적지 포트 번호 필드 (destination port number field) 이다. 각각의 포트 번호는 065535까지의 16비트 정수다. 그중에서 01023까지의 포트 번호를 잘 알려진 포트 번호 (well-known port number) 라고 하여 사용을 엄격하게 제한하고 있다. 새 애플리케이션을 개발할 때는 애플리케이션에 포트 번호를 반드시 할당해야 한다.

호스트의 각 소켓은 포트 번호를 할당 받으며, 세그먼트가 호스트에 도착하면 트랜스포트 계층은 세그먼트 안의 목적지 포트 번호를 검사하고 상응하는 소켓으로 세그먼트를 보내게 된다. 세그먼트의 데이터는 소켓을 통해 해당되는 프로세스로 전달된다. 이것은 UDP의 기본적인 동작 방식이며 TCP의 다중화/역다중화는 좀 더 많은 의미를 갖고 있다.

비연결형 다중화와 역다중화

Python 에서는 다음과 같이 UDP 소켓을 생성할 수 있다.

clientSocket = socket(AF_INET, SOCK_DGRAM)

이 방법으로 UDP 소켓이 생성될 때 트랜스포트 계층은 포트 번호를 소켓에게 자동으로 할당한다. 특히 트랜스포트 계층은 현재 호스트에서 UDP 포트로 사용하지 않는 1024~65535 사이의 포트 번호를 할당한다. 다른 방법으로는 우리가 소켓을 생성한 뒤에, 소켓 bind() 방식을 사용하여 특정 포트 번호를 UDP 소켓에 할당하기 위해 다음과 같이 작성할 수 있다.

clientSocket.bind(('', 19157))

만약 코드를 작성하는 애플리케이션 개발자가 ‘잘 알려진 프로토콜’ 의 서버 측을 구현하고 있다면, 개발자는 상응하는 ‘잘 알려진 포트 번호’를 할당해야만 한다. 일반적으로 애플리케이션의 서버 측이 특정 포트 번호를 할당하는 것에 반해, 애플리케이션의 클라이언트 측은 트랜스포트 계층이 포트 번호를 자동으로 할당한다.

UDP 소켓으로 할당된 포트 번호로 UDP 다중화/역다중화는 다음과 같다.
UDP 소켓 19157 을 가진 호스트 A의 프로세스가 호스트 B의 UDP 소켓 46428을 가진 프로세스에게 애플리케이션 데이터 전송을 원한다고 가정하자. 호스트 A의 트랜스포트 계층은 애플리케이션 데이터, 출발지 포트 번호(19157), 목적지 포트 번호(46428), 그리고 2개의 다른 값을 포함하는 트랜스포트 계층 세그먼트를 생성한다. 트랜스포트 계층은 만들어진 세그먼트를 네트워크 계층으로 전달한다. 네트워크 계층은 세그먼트를 IP 데이터그램으로 캡슐화하고 최선형 전달 서비스로 세그먼트를 수신 호스트로 전달한다. 세그먼트가 수신 호스트 B에 도착하면, 수신 호스트는 세그먼트 안의 목적지 포트 번호 (46428)을 검사하고 그 세그먼트를 포트 46428로 식별되는 소켓에 전달한다. 네트워크로부터 UDP 세그먼트들이 도착하면, 호스트 B는 세그먼트의 목적지 포트를 검사하여 세그먼트를 적절한 소켓으로 보낸다. (역다중화)

출발지 IP 주소와 출발지 포트 번호가 모두 다르거나 출발지 IP 주소와 출발지 포트 번호 중 어느 하나가 다를지라도, 같은 목적지 IP 주소와 목적지 포트 번호를 가지면 2개의 세그먼트는 같은 목적지 소켓을 통해 같은 프로세스로 향할 것이다.

왜 UDP 는 출발지 IP 주소와 출발지 포트 번호로 호스트를 식별하지 않을까?

(GPT 답변)
UDP (User Datagram Protocol)는 인터넷 프로토콜 스위트의 핵심 멤버 중 하나로, 주로 간단하고 낮은 지연시간을 요구하는 애플리케이션에서 사용됩니다. UDP가 출발지 IP 주소와 출발지 포트 번호로 호스트를 식별하지 않는 것이 아니라, UDP는 이러한 정보를 포함하지만, 연결 상태를 유지하거나 세션을 관리하지 않는 비연결형 프로토콜입니다. 이로 인해 UDP의 특성과 사용 사례가 결정됩니다:

1. **비연결성**: UDP는 연결을 설정하거나 종료하는 과정이 없습니다. 각 UDP 데이터그램은 독립적으로 처리되며, 이전 또는 이후의 데이터그램과 연관되지 않습니다. 이것은 네트워크 오버헤드를 줄여주고 데이터 전송 속도를 높입니다.
    
2. **상태 비저장**: UDP는 상태나 세션 정보를 유지하지 않습니다. 따라서 UDP는 각 데이터그램이 독립적으로 송수신되는 특성을 가집니다. 이는 서버가 동시에 많은 클라이언트와 통신할 때 유리합니다.
    
3. **간소화된 오류 처리**: UDP는 패킷 손실, 순서 변경, 중복 패킷 등에 대해 복구하지 않습니다. 오류 복구 및 순서 보장은 애플리케이션 레벨에서 처리됩니다. 이것은 리소스를 적게 소모하지만, 신뢰성이 중요한 애플리케이션에는 적합하지 않을 수 있습니다.
    
4. **비신뢰성과 단순성**: UDP는 패킷을 보낸 후 수신 여부를 확인하지 않으며, 데이터 전송에 대한 확인 응답(ACK) 또한 없습니다. 이로 인해 구현이 간단하고, 실시간 스트리밍, 온라인 게임, DNS 조회와 같은 실시간 특성이 요구되는 애플리케이션에서 선호됩니다.
    

UDP는 출발지 IP 주소와 출발지 포트 번호를 사용하여 송신자를 식별할 수 있지만, TCP와 달리 이러한 정보를 기반으로 한 연결 상태를 유지하거나 관리하지 않습니다. 따라서 UDP는 효율적이고 빠른 데이터 전송을 제공하지만, 신뢰성과 데이터 순서 보장이 필요한 애플리케이션에는 적합하지 않을 수 있습니다.

출발지 포트 번호는 ‘회신 주소’ 의 한 부분으로 사용된다. 즉, B가 세그먼트를 다시 A에게 보내기를 원할 때, B에서 A로 가는 세그먼트의 목적지 포트 번호는 A로부터 B로 가는 세그먼트의 출발지 포트 번호로부터 가져온다.

포트 번호가 모두 할당되었을 때는 어떻게 할까?

모든 포트 번호가 할당되었다는 것은 시스템에서 사용 가능한 UDP 포트가 모두 다른 프로세스나 서비스에 의해 사용 중이라는 것을 의미하므로, 이런 상황에서는 더 이상 새로운 소켓을 열 수 없다. 이러한 상황은 매우 드물지만, 이런 일이 발생하는 경우 다음과 같은 방법을 사용할 수 있다. 

1. 필요하지 않은 소켓이나 프로세스를 종료하여 포트를 해제한다. 
2. 특정 애플리케이션에서는 포트를 공유하거나 동적으로 포트를 할당받는 방식을 고려할 수도 있다. 
목적지의 포트 번호는 어떻게 알아낼 수 있을까?

목적지가 서버인 경우, 서버에서 정의한 포트 번호로 세그먼트를 전달한다. nginx 같은 리버스 프록시 서버에서 포트 번호를 변경해줄 수도 있다. 반대로 서버가 데이터를 다시 돌려보낼 때는 처음에 보냈던 출발지 IP 와 출발지 포트 번호를 사용해서 데이터를 전달할 수 있을 것이다. 

연결지향형 다중화와 역다중화

TCP 역다중화를 수행하기 위해서는 TCP 소켓과 TCP 연결 설정을 살펴봐야 한다 .TCP 소켓과 UDP 소켓의 다른 점은 TCP 소켓은 4개 요소의 집합, 즉, 출발지 IP 주소, 출발지 포트 번호, 목적지 IP 주소, 목적지 포트 번호에 의해 식별된다는 것이다. 그러므로 네트워크로부터 호스트에 TCP 세그먼트가 도착하면, 호스트는 해당하는 소켓으로 세그먼트를 전달하기 위해 4개의 값을 모두 사용한다.
특히 UDP 와는 다르게, 다른 출발 주소 또는 다른 출발지 포트 번호를 가지고 도착하는 2개의 TCP 세그먼트는 2개의 다른 소켓으로 향하게 된다.

  • TCP 서버 애플리케이션은 ‘환영 소켓’ 을 갖고 있다. 이 소켓은 포트 번호 12000을 가진 TCP 클라이언트로부터 연결 설정 요청을 기다린다.
  • TCP 클라이언트는 다음과 같은 명령으로 소켓을 생성하고 연결 설정 요청 세그먼트를 보낸다.
clientSocket = socket(AF_INET, SOCK_STREAM)
clientSocket.connect((serverName, 12000))
  • 연결 설정 요청은 목적지 포트 번호 12000과 TCP 헤더에 설정된 특별한 연결 설정 비트를 가진 TCP 세그먼트에 지나지 않는다. 또한 그 세그먼트는 출발지 포트 번호를 포함하는데, 이 출발지 포트 번호는 클라이언트가 선택한 번호다.
  • 서버 프로세스로 동작하는 컴퓨터의 호스트 운영체제가 목적지 포트 12000을 포함하는 연결 요청 세그먼트를 수신하면, 이 세그먼트를 포트 번호 12000으로 연결 수락을 기다리는 서버 프로세스로 보낸다.
connectionSocket, addr = serverSocket.accept()
  • 서버는 연결 요청 세그먼트의 출발지 포트 번호, 출발지 호스트의 IP 주소, 세그먼트 안의 목적지 포트 번호, 목적지 IP 주소에 주목하고, 새롭게 생성된 연결 소켓은 이 값들로 식별된다. 그 다음에 도착하는 모든 세그먼트의 출발지 포트, 출발지 IP 주소, 목적지 포트, 목적지 IP 주소가 이 네 가지 값과 일치하면 세그먼트는 해당 소켓으로 역다중화될 것이다.

서버 호스트는 동시에 존재하는 많은 TCP 소켓을 지원할 수 있다. 해당 소켓은 프로세스에 접속되어 있으며, 소켓은 4개 요소의 집합에 의해 식별된다.

웹 서버와 TCP

아파치 웹 서버 같은 웹 서버가 포트 번호 80 상에서 동작하는 호스트를 고려해보자. 클라이언트가 서버로 세그먼트를 보내면, 모든 세그먼트는 목적지 포트 번호 80을 가지고 있을 것이다. 특히, 초기 연결 설정 세그먼트들과 HTTP 요청 메시지를 전달하는 모든 세그먼트는 목적지 포트 80을 가질 것이다. 앞에서 살펴본 것처럼, 서버는 각기 다른 클라이언트가 보낸 세그먼트를 출발지 IP 주소와 출발지 포트 번호로 구별한다.

웹 서버는 각각의 연결에 대해 새로운 프로세스를 만들고, 해당 프로세스들은 각자 연결 소켓을 가지며 이 연결 소켓을 통해 HTTP 요청을 수신하고, HTTP 응답을 전송한다.
그러나 연결 소켓과 프로세스 사이에 항상 1:1 대응이 이루어지는 것은 아니다. 실제로 오늘날의 많은 고성능 웹 서버는 하나의 프로세스만 사용한다. 그러면서, 각각의 새로운 클라이언트 연결을 위해 새로운 연결 소켓과 함께 새로운 스레드를 생성한다. 그런 웹 서버에서는 하나의 같은 프로세스에 붙어 있는 많은 연결 소켓들이 동시에 존재할 수도 있다.

만약 클라이언트와 서버가 지속적인 HTTP 를 사용한다면, 지속적인 연결의 존속 기간에 클라이언트와 서버는 같은 서버 소켓을 통해 HTTP 메시지를 교환할 것이다. 그러나 만약 클라이언트와 서버가 비지속적인 HTTP를 사용한다면, 모든 요청/응답 마다 새로운 TCP 연결이 생성되고 종료될 것이다. 이 빈번하게 발생하는 소켓 생성과 종료는 웹 서버 성능에 심한 부담을 준다.

비연결형 트랜스포트: UDP

UDP 는 트랜스포트 계층 프로토콜이 할 수 있는 최소 기능으로 동작한다. UDP 는 다중화/역다중화 기능과 간단한 오류 검사 기능을 제외하면 IP에 아무것도 추가하지 않는다. UDP 는 애플리케이션 프로세스로부터 메시지를 가져와서 다중화/역다중화 서비스에 대한 출발지 포트 번호 필드와 목적지 포트 번호 필드를 첨부하고 다른 두 필드를 추가한 후에 최종 세그먼트를 네트워크 계층으로 넘겨준다. 네트워크 계층은 트랜스포트 계층 세그먼트를 IP 데이터그램으로 캡슐화하고, 세그먼트를 수신 호스트에 전달하기 위해 최선을 다한다. 만약 세그먼트가 수신 호스트에 도착한다면, UDP 는 세그먼트의 데이터를 해당하는 애플리케이션 프로세스로 전달하기 위해 목적지 포트 번호를 사용한다. UDP 는 세그먼트를 송신하기 전에 송신 트랜스포트 계층 개체들과 수신 트랜스포트 계층 개체들 사이에 핸드 셰이크를 사용하지 않는다. 이런 이유로 UDP 를 비연결형 이라고 한다.

DNS 는 전형적으로 UDP 를 사용하는 애플리케이션 계층 프로토콜이다. 호스트의 DNS 애플리케이션이 질의를 생성할 때, DNS 질의 메시지를 작성하고 UDP에게 메시지를 넘겨준다. 목적지 종단 시스템 상에서 동작하는 UDP 개체와 호스트 측 UDP 는 어떠한 핸드셰이크도 수행하지 않고 메시지에 헤더 필드를 추가한 후에 최종 세그먼트를 네트워크 계층에 넘겨준다. 네트워크 계층은 UDP 세그먼트를 데이터그램으로 캡슐화하고 네임 서버에 데이터그램을 송신한다. 이때 질의 호스트에서의 DNS 애플리케이션은 질의에 대한 응답을 기다린다. 만약 질의 호스트가 응답을 수신하지 못하면, 질의를 다른 네임 서버로 송신하거나 이를 야기한 애플리케이션에게 응답을 수신할 수 없음을 통보한다.

많은 애플리케이션은 다음과 같은 이유로 UDP 에 더 적합하다.

  • 무슨 데이터를 언제 보낼지에 대해 애플리케이션 레벨에서 더 정교한 제어를 할 수 있다: 애플리케이션 프로세스가 데이터를 UDP 에 전달하자마자 UDP 는 데이터를 UDP 세그먼트로 만들고, 그 세그먼트를 즉시 네트워크 계층으로 전달한다. 이에 반해 TCP 는 혼잡 제어 메커니즘을 갖고 있다. 이 혼잡 제어 메커니즘은 목적지 호스트들과 출발지 호스트들 사이에서 하나 이상의 링크가 과도하게 혼잡해지면, 트랜스포트 계층 TCP 송신자를 제한한다. 또한 TCP 는 신뢰적인 전달이 얼마나 오래 걸리는지에 관계없이 목적지가 세그먼트의 수신 여부를 확인, 응답할 때까지 데이터의 세그먼트 재전송을 계속할 것이다. 실시간 애플리케이션은 최소 전송률을 요구할 때도 있고, 지나치게 지연되는 세그먼트 전송을 원하지 않으며, 조금의 데이터 손실은 허용할 수도 있으므로 TCP 의 서비스 모델은 이러한 애플리케이션의 요구와는 맞지 않는다.
  • 연결 설정이 없음: UDP 는 연결을 설정하기 위한 어떤 지연도 없다.
  • 연결 상태가 없음: UDP 는 연결 상태를 유지하지 않으며 파라미터 중 어떤 것도 기록하지 않는다. 이러한 이유로 일반적으로 특정 애플리케이션 전용 서버는 애플리케이션 프로그램이 TCP보다 UDP에서 동작할 때 일반적으로 좀 더 많은 액티브 클라이언트를 수용할 수 있다.
  • 작은 패킷 헤더 오버헤드: TCP 는 세그먼트마다 20바이트의 헤더 오버헤드를 갖지만, UDP 는 단지 8바이트의 오버헤드를 갖는다.

UDP 세그먼트 구조

애플리케이션 데이터는 UDP 데이터그램의 데이터 필드에 위치한다. UDP 헤더는 2바이트씩 구성된 단 4개의 필드만을 갖는다. 포트 번호는 목적지 호스트가 목적지 종단 시스템에서 동작하는 정확한 프로세스에게 애플리케이션 데이터를 넘기게 해준다. 체크섬은 세그먼트에 오류가 발생했는지를 검사하기 위해 수신 호스트가 사용한다. 길이 필드는 헤더를 포함하는 UDP 세그먼트의 길이를 나타낸다.

UDP 체크섬

UDP 체크섬은 세그먼트가 출발지로부터 목적지로 이동했을 때, UDP 세그먼트 안의 비트에 대한 변경 사항이 있는지 검사하는 것이다. 송신자는 체크섬 안에 있는 모든 16비트 워드의 합산에 대해 다시 1의 보수를 수행하며, 합산 과정에서 발생하는 오버플로는 윤회식 자리 올림(wrap around)을 한다.

만약 패킷에 어떤 오류도 없다면 수신자에서의 합은 모든 자리가 1이 될 것이다. 비트 중에 하나라도 0이 있다면 패킷에 오류가 발생했음을 알 수 있다.

UDP 가 체크섬을 제공하는 이유는 출발지와 목적지 사이의 모든 링크가 오류 검사를 제공한다는 보장이 없기 떄문이다. 그러므로 세그먼트들이 정확하게 링크를 통해 전송되었을지라도, 세그먼트가 라우터의 메모리에 저장될 때 비트 오류가 발생할 수 있다. 주어진 링크 간의 신뢰성과 메모리의 오류 검사가 보장되지도 않고, 종단 간의 데이터 전송 서비스가 오류 검사를 제공해야 한다면, UDP 는 종단 기반으로 트랜스포트 계층에서 오류 검사를 제공해야만 한다. 이것이 시스템 설계에서의 종단과 종단의 원칙 (end-end principle) 이다.

IP는 어떠한 2계층 프로토콜에서도 동작해야 하므로, 트랜스포트 계층은 안전장치로서 오류 검사를 제공하는 것이 유용하다. UDP 는 오류 검사를 제공하지만, 오류를 회복하기 위한 어떠한 일도 하지 않는다.

신뢰적인 데이터 전송의 원리

신뢰적인 채널에서는 전송된 데이터가 손상되거나 손실되지 않는다. 그리고 모든 데이터는 전송된 순서 그대로 전달된다. 이것이 TCP가 인터넷 애플리케이션에게 제공하는 서비스 모델이다.

이러한 서비스 추상화를 구현하는 것이 신뢰적인 데이터 전송 프로토콜 (reliable data transfer protocol) 의 의무이다. 이 작업은 신뢰적인 전송 프로토콜의 아래에 있는 계층이 신뢰적이지 않을 수 있어서 어려워진다.

여기서는 단방향 데이터 전송 의 경우인 송신 측으로부터 수신 측까지의 데이터 전송만을 고려한다.

신뢰적인 데이터 전송 프로토콜의 구축

완벽하게 신뢰적인 채널상에서의 신뢰적인 데이터 전송: rdt1.0

먼저, 하위 채널이 완전히 신뢰적인 가장 간단한 경우를 고려한다.

all-reliable-channel.jpeg

이러한 간단한 프로토콜에서는 데이터 단위와 패킷의 차이점이 없다. 또한 모든 패킷의 흐름은 송신자로부터 수신자까지다. 즉, 완전히 신뢰적인 채널에서는 오류가 생길 수 없으므로 수신 측이 송신측에게 어떤 피드백도 제공할 필요가 없다. 또한 수신자는 송신자가 데이터를 송신하자마자 데이터를 수신할 수 있다고 가정했으므로 수신자가 송신자에게 천천히 보내라는 것을 요청할 필요가 없다.

비트 오류가 있는 채널상에서의 신뢰적 데이터 전송: rdt2.0

패킷 안의 비트들이 하위 채널에서 손상되는 모델이다. 전송된 모든 패킷이 송신된 순서대로 수신된다고 계속 가정한다.

컴퓨터 네트워크 설정에서 재전송을 기반으로 하는 신뢰적인 데이터 전송 프로토콜은 자동 재전송 요구 (Automatic Repeat reQuest, ARQ) 프로토콜 로 알려져 있다.

다음과 같은 세 가지 부가 프로토콜 기능이 ARQ 프로토콜에 요구된다.

  • 오류 검출: 비트 오류가 발생했을 때 수신자가 검출할 수 있는 기능이 필요하다.
  • 수신자 피드백: rdt2.0 프로토콜은 수신자로부터 송신자 쪽으로 ACK 와 NAK 패킷들을 전송할 것이다.
  • 재전송: 수신자에서 오류를 가지고 수신된 패킷은 송신자에 의해 재전송된다.

bit-error-channel.jpeg

가장 왼쪽 상태에서 송신 측 프로토콜은 상위 계층에서 데이터가 전달되기를 기다린다. rdt_send 이벤트가 발생하면 송신자는 패킷 체크섬과 함께 전송될 데이터를 포함하는 패킷을 생성하고, 패킷을 udt_send 로 전송할 것이다. 가장 오른쪽 상태에서 송신자 프로토콜은 수신자로부터의 ACK, NAK 패킷을 기다린다. ACK 패킷이 수신된다면 송신자는 가장 최근에 전송된 패킷이 정확하게 수신되었음을 알게 된다. NAK 가 수신되면 프로토콜은 마지막 패킷을 재전송하고 재전송된 데이터 패킷에 대한 응답으로 수신자에 의해 응답하는 ACK, NAK 를 기다린다. 송신자는 수신자가 현재의 패킷을 정확하게 수신했음을 확신하기 전까지 새로운 데이터를 전달하지 않을 것이다. 이러한 행동 때문에 rdt2.0과 같은 프로토콜은 전송 후 대기 (stop-and-wait) 프로토콜로 알려져 있다.
수신자 측 FSM 은 아직 단일 상태를 갖는다.

프로토콜 rdt2.0은 잘 동작하는 것 같지만 실제로는 치명적인 결함이 있다.
==ACK, NAK 패킷이 손상되는 경우==는 어떻게 해야 할까? 최소한 그런 오류를 검출하기 위해 ACK, NAK 패킷에 대한 체크섬 비트를 추가할 필요가 있다. 더 어려운 점은 어떻게 프로토콜이 ACK 또는 NAK 패킷 오류로부터 복구되는가이다. ACK, NAK가 손상된다면 송신자는 수신자가 전송된 데이터의 마지막 부분을 올바르게 수신했는지를 알 방법이 없다.

다음과 같은 세 가지 가능성을 고려해보자.

  1. 수신자의 응답을 이해하지 못했을 때를 위한 새로운 송신자-수신자 패킷을 도입한다.
  2. 송신자가 검출뿐만 아니라 비트 오류로부터 회복할 수 있도록 충분한 체크섬 비트들을 추가한다.
  3. 송신자가 왜곡된 ACK, NAK 패킷을 수신할 때 현재 데이터 패킷을 단순히 다시 송신하는 것이다. 그러나 이 방식은 송신자에서 수신자 간의 채널로 중복 패킷을 전송하게 된다. 이 경우, 마지막으로 전송된 ACK, NAK가 송신자에게 정확히 수신되었는지를 알 수 없다. 그러므로 도착하는 패킷이 새로운 데이터를 포함하고 있는 것인지 재전송인지를 사전에 알 수 없다.

이 문제의 간단한 해결책은 데이터 패킷에 새로운 필드를 추가하고 이 필드 안에 순서 번호를 삽입하는 방식으로 데이터 패킷에 송신자가 번호를 붙이는 것이다. 일반적으로 패킷을 손실하지 않는 채널을 가정하고 있으므로, ACK와 NAK 패킷 자체는 확인 중인 패킷의 순서 번호를 나타낼 필요는 없다. 송신자는 수신된 ACK와 NAK 패킷이 가장 최근에 전송된 데이터 패킷에 대한 응답으로 발생한 것임을 알 수 있다.

bit-error-numbering-sender.jpeg

bit-error-numbering-receiver.jpeg

프로토콜 rdt2.1은 수신자로부터 송신자까지의 긍정 확인 응답과 부정 확인응답을 모두 포함한다. 순서가 바뀐 패킷이 수신되면, 수신자는 이미 전에 수신한 패킷에 대한 긍정 확인응답을 전송한다. 손상된 패킷이 수신되면, 수신자는 부정 확인응답을 전송한다. NAK를 송신하는 것 대신에, 가장 최근에 정확하게 수신된 패킷에 대해 ACK를 송신함으로써 NAK 를 송신한 것과 같은 효과를 얻을 수 있다.

비트 오류를 갖는 채널을 위한 NAK 없는 신뢰적인 데이터 전송 프로토콜은 rdt2.2이다.
rtd2.1과 rtd2.2의 미묘한 차이는 수신자가 반드시 ACK 메시지에 의해 확인 응답되는 패킷의 순서 번호를 포함해야 한다는 점이다. 그리고 송신자는 수신된 ACK 메시지에 의해 확인응답된 패킷의 순서 번호를 반드시 검사해야만 한다.

bit-error-absent-nak.jpeg

비트 오류와 손실 있는 채널상에서의 신뢰적인 데이터 전송: rdt3.0

비트가 손상되는 것 외에도 인터넷을 포함하는 오늘날의 컴퓨터 네트워크처럼 하위 채널이 패킷을 손실하는 경우를 생각해보자.

  1. 어떻게 패킷 손실을 검출할 것인가
  2. 패킷 손실이 발생했을 때 어떤 행동을 할 것인가

패킷 손실을 다룰 때 여러 가지 접근이 가능하지만, 송신자에게 손실된 패킷의 검출과 회복 책임을 부여해보자.
송신자가 데이터 패킷을 전송하고 패킷 또는 수신자의 패킷에 대한 ACK 를 손실했다고 가정해보자. 만약 송신자가 패킷을 잃어버렸다고 확신할 정도의 충분한 시간을 기다릴 수만 있다면 데이터 패킷은 간단히 재전송될 수 있다.

그러나 송신자가 어떤 패킷을 손실했다는 것을 확신하기 위해 얼마나 오랫동안 기다려야 할까? 송신자는 적어도 송신자와 수신자 사이의 왕복 시간 지연에 수신 측에서 패킷을 처리하는 데 필요한 시간을 더한 만큼 기다린다. 많은 네트워크에서는 이 최악의 최대 지연 시간은 예측하기조차도 매우 어렵다. 실제 상황에서 채택한 접근 방식은 송신자가 패킷 손실이 일어났다는 보장은 없지만 손실이 일어났을 만한 그런 시간을 현명하게 선택하는 것이다.
만약 패킷이 유별나게 큰 지연을 갖는다면, 송신자는 비록 데이터 패킷이나 그 패킷에 대한 ACK가 손실되지 않았다고 하더라도 패킷을 재전송할 수 있다. 이것은 송신자 대 수신자 채널에서 중복 데이터 패킷의 가능성을 포함한다.

시간 기반의 재전송 메커니즘을 구현하기 위해, 주어진 시간이 지난 후에 송신자를 인터럽트할 수 있는 카운트 다운 타이머 가 필요하다.

  1. 매 패킷 (첫 번째 또는 재전송 패킷)이 송신된 시간에 타이머를 시작함
  2. 타이머 인터럽트에 반응함(적당한 행동을 취함)
  3. 타이머를 멈춤

bit-error-alternating-bit.jpeg

패킷에 대한 수신 시간은 전송 지연과 전파 지연 때문에 패킷 전송 시간보다 더 늦다. 패킷의 순서 번호가 0과 1이 번갈아 일어나므로, 프로토콜 rdt3.0은 얼터네이팅 비트 프로토콜 (alternating-bit protocol) 이라고 부른다.

파이프라이닝된 신뢰적인 데이터 전송 프로토콜

rdt 3.0의 핵심적인 성능 문제는 stop-and-wait 프로토콜이라는 점이다.

전송 후 대기 방식으로 동작하는 대신, 송신자에게 확인 응답을 기다리지 않고 여러 패킷을 전송하도록 허용하면 성능 문제를 간단히 해결할 수 있다. 많은 전송 중인 송신자-수신자 패킷을 파이프라인에 채워넣음으로써 나타낼 수 있다. 이 기술을 파이프라이닝 이라고 부른다.

파이프라이닝은 신뢰적인 데이터 전송 프로토콜에서 다음과 같은 중요성을 지니고 있다.

  • 순서 번호의 범위가 커져야 한다. 각각의 전송 중인 패킷은 유일한 순서 번호를 가져야 하고 전송 중인 확인 응답(ACK)이 안 된 패킷이 여럿 있을 수도 있기 때문이다.
  • 프로토콜의 송신 측과 수신 측은 패킷 하나 이상을 버퍼링해야 한다. 최소한 송신자는 전송되었으나 확인응답되지 않은 패킷을 버퍼링해야 한다. 정확하게 수신된 패킷의 버퍼링은 다음에 설명한 것처럼 수신자에게서도 필요하다.
  • 필요한 순서 번호의 범위와 버퍼링 조건은 데이터 전송 프로토콜이 손실 패킷과 손상 패킷 그리고 상당히 지연된 패킷들에 대해 응답하는 방식에 달려 있다. 파이프라인 오류 회복의 두 가지 기본적인 접근 방법으로 GBN(Go-Back-N), SR(Selective Repeat) 등이 있다.

GBN

GBN 프로토콜 에서 송신자는 확인응답을 기다리지 않고 여러 패킷을 전송할 수 있다. 그러나 파이프라인에서 확인응답이 안 된 패킷의 최대 허용 수 N보다 크지 말아야 한다.

확인응답이 안 된 가장 오래된 패킷의 순서 번호를 base 로 정의하고 사용되지 않은 가장 작은 순서 번호를 nextseqnum 라고 정의한다면 순서 번호의 범위에서 4개의 간격을 식별할 수 있다. 간격 [0, base - 1] 에서 순서 번호는 이미 전송되고 확인응답이 된 패킷에 대응된다. 간격 [base, nextseqnum -1] 은 송신은 되었지만 아직 확인 응답되지 않은 패킷에 대응된다. 간격 [nextseqnum, base + N-1] 은 상위 계층으로부터 데이터가 도착하면 바로 전송될 수 있는 패킷을 위해 사용될 수 있다. 마지막으로, base+N 이상의 순서 번호는 파이프라인에서 확인응답이 안 된 패킷의 확인응답이 도착할 때까지 사용될 수 없다.

아직 확인응답이 안 된 패킷을 위해 허용할 수 있는 순서 번호의 범위는 순서 번호의 범위 상에서 크기가 NN 인 ‘윈도’로 나타낸다. 프로토콜이 동작할 때, 이 윈도는 순서 번호 공간에서 오른쪽으로 이동된다. 이러한 이유로 NN윈도 크기 라고 부르며, GBN 프로토콜은 슬라이딩 윈도 프로토콜 (sliding-window protocol) 이라고 부른다.

그렇다면 왜 확인응답이 안 된 패킷의 수를 무한한 번호를 허용하지 않을까? 실제로 패킷의 순서 번호는 패킷 헤더 안의 고정된 길이 필드에 포함된다. 만약 kk 가 패킷 순서 번호 필드의 비트 수라면, 순서 번호의 범위는 [0,2k1][0, 2^{k}- 1] 이 된다. 순서 번호의 제한된 범위에서, 순서 번호를 포함하는 모든 계산은 모듈로 2k2^k 연산을 사용한다.

아래의 FSM은 base, nextseqnum 변수에서의 동작과 이러한 변수를 포함하는 조건부 동작을 추가했으므로 이 FSM 을 확장된 FSM 이라고 한다.

gbn-extended-fsm.jpeg

gbn-receiver-fsm.jpeg

GBN 송신자는 다음과 같은 세 가지 타입의 이벤트에 반응해야 한다.

  • 상위로부터의 호출: rdt_send 가 위로부터 호출되면, 송신자는 우선 윈도가 가득 찼는지, 즉 N개의 아직 확인응답되지 않은 패킷이 있는지를 확인한다. 만약 윈도가 가득 차 있지 않다면 패킷이 생성되고 송신된다. 그리고 변수들이 적절히 갱신된다. 만약 윈도가 가득 차 있다면, 송신자는 윈도가 가득 차 있음을 가리키는 함축적인 의미로 단지 데이터를 상위 계층으로 반환한다. 상위 계층은 나중에 다시 시도할 것이다. 실제적인 구현에서 송신자는 해당 데이터를 버퍼링하거나, 오직 윈도가 가득 차 있지 않을 때만 rdt_send() 를 호출하는 동기화 메커니즘 (세마포어나 flag) 을 사용할 것이다.
  • ACK의 수신: GBN 프로토콜에서 순서 번호 n을 가진 패킷에 대한 확인응답은 누적 확인응답 (cumulative acknowledgment) 으로 인식된다. 이 누적 확인응답은 수신 측에서 올바르게 수신된 nn 을 포함하여 nn 까지의 순서 번호를 가진 모든 패킷에 대한 확인 응답이다.
  • 타임아웃 이벤트: 전송 후 대기 프로토콜에서와 같이 타이머는 손실된 데이터 또는 손실된 확인응답 패킷으로부터 회복하는 데 사용된다. 만약 타임아웃이 발생한다면, 송신자는 이전에 전송되었지만 아직 확인응답되지 않은 모든 패킷을 다시 송신한다. 송신자는 가장 오래된 ‘전송했지만, 아직 확인응답 안 된 패킷’ 에 대한 타이머로 생각될 수 있는 단일 타이머를 사용한다. 만일 한 ACK가 수신되었지만 추가로 ‘전송했지만, 아직 확인응답 안 된 패킷’이 아직 존재한다면, 타이머는 다시 시작된다. 만약 아직 확인응답 안 된 패킷이 없다면, 타이머는 멈춘다.

GBN 에서는 수신자의 행동도 단순하다. 만약 순서 번호 n 을 가진 패킷이 오류 없이, 그리고 순서대로 수신된다면(즉, 상위 계층에 마지막으로 전달된 데이터가 순서 번호 n-1 을 가진 패킷으로부터 온 것이라면), 수신자는 패킷 n에 대한 ACK를 송신하고 상위 계층에 패킷의 데이터 부분을 전달한다. 그 외의 경우에는 수신자는 그 패킷을 버리고 가장 최근에 제대로 수신된 순서의 패킷에 대한 ACK를 재전송한다. 패킷이 상위 계층에 한 번에 하나씩 전송되므로, 만일 패킷 k가 수신되고 상위 계층에 전달되었다면, 이때는 이미 k보다 낮은 순서 번호를 가진 모든 패킷 또한 제대로 전달되어 있다는 뜻이다.

GBN 프로토콜에서 수신자는 순서가 잘못된 패킷들을 버린다. 비록 정확하게 수신된 패킷을 버리는 것이 낭비인 것 같지만, 이런 작업을 수행하는 데는 몇 가지 당연한 이유가 있다.
지금 패킷 n이 수신되어야 하지만, 그 사람 다음의 패킷 n + 1 이 먼저 도착했다고 가정하자. 데이터가 순서대로 전달되어야 하므로, 수신자는 패킷 n + 1을 저장하고 나중에 패킷 n이 수신되고 전달된 후에 상위 계층에 이 패킷을 전달한다. 그러나 만일 패킷 n이 손실된다면 송신자에서 GBN 재전송 규칙에 따라 패킷 n과 n + 1 이 모두 재전송될 것이다. 이런 접근의 이점은 수신자 버퍼링이 간단하다는 것이다. 즉, 수신자는 어떤 순서가 잘못된 패킷에 대해 버퍼링을 할 필요가 없다. 그러므로 송신자는 윈도 상위와 하위 경계와 이 윈도 안에 있는 nextseqnum 위치를 유지해야 하지만, 수신자가 유지해야 하는 것은 단지 다음 순서 패킷의 순서 번호다. 물론, 올바르게 수신된 패킷을 버리는 것의 단점은 그 패킷의 재전송이 손실되거나 왜곡될 수 있으므로 많은 재전송이 필요할 수도 있다는 것이다.

어떤 한 프로토콜 스택에서 이러한 프로토콜의 구현은 확장된 FSM의 구현과 유사한 구조를 갖는다. 또한 이 구현은 발생할 수 있는 다양한 이벤트에 대한 대응으로 취할 수 있는 동작을 구현하는 다양한 절차들과 유사하다. 이러한 이벤트 기반 프로그래밍 (event-based programming) 에서의 다양한 프로시저들은 프로토콜 스택에서 다른 프로시저에 의해 야기되거나 인터럽트의 결과로 요청될 것이다. 송신자에서 이러한 이벤트는 rdt_send() 를 호출하기 위한 상위 계층 개체로부터의 호출, 타이머 인터럽트, 패킷이 도착했을 때 rdt_rcv() 를 호출하기 위한 하위 계층으로부터의 호출이다.

SR

GBN 프로토콜은 송신자가 패킷으로 파이프라인을 채우는 것을 가능하게 하여, 전송 후 대기 프로토콜에서 채널 이용률 문제를 피하도록 하고 있다. 그러나 GBN 자체에도 성능 문제를 겪는 시나리오들이 존재한다. 특히 윈도 크기와 대역폭 지연 (bandwidth-delay) 곱의 결과가 모두 클 때, 많은 패킷이 파이프라인에 있을 수 있다. 그러나 GBN은 패킷 하나의 오류 때문에 많은 패킷을 재전송하므로, 많은 패킷을 불필요하게 재전송하는 경우가 발생한다. 채널 오류의 확률이 증가할수록 파이프라인은 불필요한 재전송 데이터로 채워진다.

SR 프로토콜은 수신자에서 오류가 발생한 패킷을 수신했다고 의심되는 패킷만을 송신자가 다시 전송하므로 불필요한 재전송을 피한다. 필요에 따라 각각의 개별적인 재전송은 수신자가 올바르게 수신된 패킷에 대한 ‘개별적인’ ACK 를 요구할 것이다. 윈도 크기 N은 파이프라인에서 아직 확인 응답이 안 된 패킷 수를 제한하는 데 사용된다.

SR 수신자는 패킷의 순서와는 무관하게 손상 없이 수신된 패킷에 대한 확인응답을 할 것이다. 순서가 바뀐 패킷은 빠진 패킷이 수신될 때까지 버퍼에 저장하고, 빠진 패킷이 수신된 시점에서 일련의 패킷을 순서대로 상위 계층에 전달할 수 있다.

### SR 송신자 이벤트와 행동
1. 상위로부터 데이터 수신: 상위에서 데이터가 수신될 때, SR 송신자는 패킷의 다음 순서 번호를 검사한다. 순서 번호가 송신자 윈도 내에 있으면 데이터는 패킷으로 송신된다. 그렇지 않으면 GBN 처럼 버퍼에 나중에 전송하기 위해 되돌려진다.
2. 타임아웃: 타이머는 손실된 패킷을 보호하기 위해 재사용된다. 그러나 타임아웃 시 오직 한 패킷만이 전송되기 때문에, 각 패킷은 자신의 논리 타이머가 있어야 한다. 하나의 하드웨어 타이머가 여러 개의 논리 타이머를 흉내 내는 데 사용된다.
3. ACK 수신: ACK 가 수신되었을 때, SR 송신자는 그 ACK 가 윈도 내에 있다면 그 패킷을 수신된 것으로 표기한다. 만약 패킷 순서 번호가 send_base 와 같다면, 윈도 베이스는 가장 작은 순서 번호를 가진 아직 확인응답하지 않은 패킷으로 옮겨진다. 만약 윈도가 이동하고 윈도 내의 순서 번호를 가진 미전송 패킷이 있다면, 이 패킷들은 전송된다.
### SR 수신자 이벤트와 행동
1. [rcv_base, rcv_base+N-1] 내의 순서 번호를 가진 패킷이 손상 없이 수신된다. 이 경우는 수신된 패킷이 수신자의 윈도에 속하는 것이며, 선택적인 ACK 패킷이 송신자에게 회신된다. 만약 이 패킷이 이전에 수신되지 않았던 것이라면 버퍼에 저장된다. 
2. [rcv_base-N, rcv_base-1] 내의 순서 번호를 가진 패킷이 수신된다. 이 경우에는 이 패킷이 수신자가 이전에 확인응답한 것이라도 ACK 가 생성되어야 한다.
3. 그 외의 경우, 패킷을 무시한다.

SR 프로토콜에서 송신자와 수신자의 윈도는 항상 같지 않다.

너무 큰 윈도를 가진 SR 수신자의 고민: 새 패킷인가, 아니면 재전송된 것인가?

수신자 윈도우의 순서 번호 최댓값을 k, 수신자 윈도우의 크기를 N 이라고 가정한다.

수신자 윈도우의 순서 번호는 0..k 이다. 수신 이전, 수신자 윈도우는 0..N-1 의 범위를 갖는다. 새 패킷과 재전송된 패킷을 구분하기 위해서는 수신자 윈도우 내부의 순서 번호에 0이 포함되지 않아야 한다. 즉, 순서 번호가 초기화되지 않아야 한다. 모든 패킷을 수신했을 때, 위의 조건을 충족하는 수신자 윈도우는 N..2N-1 의 범위를 갖는다.

따라서 2N-1 의 값은 k보다 작거나 같아야 한다.

2N1<=k2N-1 <= k N<=k+12N <= \frac{k+1}{2}

패킷들은 송신자와 수신자 사이의 채널 안에서 순서가 바뀔 수 없다는 가정을 다시 생각해보자. 이는 일반적으로 송신자가 수신자가 단일한 물리적 선으로 연결되어 있을 때 적합한 가정이다. 그러나 둘을 연결하는 채널이 네트워크일 때는 패킷 순서 바뀜이 일어날 수 있다.

패킷 순서 바뀜 현상으로, 송신자와 수신자의 윈도가 x 를 포함하고 있지 않더라도, 순서 번호 또는 확인응답 번호 x를 가진 오래된 패킷의 복사본들이 생길 수 있다. 패킷 순서가 바뀌는 채널이라는 것은, 본질적으로 패킷들을 버퍼에 저장하고 나중에 어느 때나 이 패킷들을 임의로 내보낸다고 간주할 수 있다. 순서 번호가 재사용될 수 있으므로 중복된 패킷들을 막을 수 있는 조치가 있어야 한다.

실제 방식은 송신자가 이전에 송신된 순서 번호 x를 가진 패킷들이 더는 네트워크상에 없음을 어느 정도 확신할 때까지 순서 번호가 재사용되지 않음을 확실히 하는 것이다. 이는 패킷이 어느 일정 시간 이상으로 네트워크에서 존재할 수 없다는 가정에 의해 이루어진다. 대략 3분의 최대 패킷 수명이 고속 네트워크에 대한 TCP 확장에 가정되어 있다.

연결지향형 트랜스포트: TCP

TCP 연결

TCP 는 애플리케이션 프로세스가 데이터를 다른 프로세스에게 보내기 전에, 두 프로세스가 서로 ‘핸드셰이크’를 먼저 해야 하므로 연결지향형이다. 즉, 데이터 전송을 보장하는 파라미터들을 각자 설정하기 위한 어떤 사전 세그먼트들을 보내야 한다.

TCP 연결은 회선 교환 네트워크에서와 같은 종단간의 TDM 이나 FDM 이 아니다. 대신, ‘연결’은 두 통신 종단 시스템의 TCP 에 존재하는 상태를 공유하는 논리적인 것이다. TCP 프로토콜은 오직 종단 시스템에서만 동작하고 중간의 네트워크 요소(라우터와 링크 계층 스위치)에서는 동작하지 않으므로, 중간의 네트워크 요소들은 TCP 연결 상태를 유지하지 않는다. 사실, 중간 라우터들은 TCP 연결을 전혀 감지하지 못한다.

TCP 연결은 전이중 서비스(full-duplex service) 를 제공한다. 만약 호스트 A의 프로세스와 호스트 B의 프로세스 사이에 TCP 연결이 있다면, 애플리케이션 계층 데이터는 B에서 A로 흐르는 동시에 A에서 B로 흐를 수 있다. 또한 TCP 연결은 항상 단일 송신자와 단일 수신자 사이의 점대점이다. 단일 송신 동작으로 한 송신자가 여러 수신자에게 데이터를 전송하는 멀티캐스팅은 TCP에서는 불가능하다.

TCP 연결은 어떻게 설정될까. 한 호스트에서 동작하는 프로세스가 다른 호스트의 프로세스와 연결을 초기화하기를 원한다고 가정하자. 연결을 초기화하는 프로세스를 클라이언트 프로세스, 다른 프로세스를 서버 프로세스라고 부른다고 했다. 클라이언트 애플리케이션 프로세스는 서버의 프로세스와 연결을 설정하기를 원한다고 TCP 클라이언트에게 특별한 TCP 세그먼트를 보낸다. 서버는 두 번째 특별한 TCP 세그먼트로 응답한다. 마지막으로, 클라이언트가 세 번째 특별한 세그먼트로 다시 응답한다.

처음 2개의 세그먼트에는 페이로드, 즉 애플리케이션 계층 데이터가 없다. 세 번쨰 세그먼트는 페이로드를 포함할 수 있다. 두 호스트 사이에 3개의 세그먼트가 보내지므로, 이 연결 설정 절차는 흔히 세 방향 핸드셰이크(three-way handshake) 라 부른다.

일단 TCP 연결이 설정되면, 두 애플리케이션 프로세스는 서로 데이터를 보낼 수 있다. 클라이언트 프로세스에서 서버 프로세스로의 데이터 송신을 고려해보자. 클라이언트 프로세스는 소켓을 통해 데이터의 스트림을 전달한다. 데이터가 관문을 통해 전달되면, 이제 데이터는 클라이언트에서 동작하고 있는 TCP 에 맡겨진다. TCP 는 초기 three-way handshake 동안 준비된 버퍼 중 하나인 연결의 송신 버퍼(send buffer) 로 데이터를 보낸다. 때때로 TCP 는 송신 버퍼에서 데이터 묶음을 만들어서 네트워크로 보낸다. TCP 명세서에서 TCP가 언제 버퍼된 데이터를 전송해야 하는지 기술하고 있지 않다. 세그먼트로 모아 담을 수 있는 최대 데이터의 양은 최대 세그먼트 크기(maximum segment size, MSS) 로 제한된다. MSS는 일반적으로 로컬 송신 호스트에 의해 전송될 수 있는 가장 큰 링크 계층 프레임의 길이(최대 전송 단위(maximum transmission unit, MTU))에 의해 일단 결정되고, 그런 후에 TCP 세그먼트와 TCP/IP 헤더 길이(통상 40바이트)가 단일 링크 계층 프레임에 의해 딱 맞도록 하여 정해진다. 이더넷과 PPP 링크 계층 프로토콜은 모두 1500바이트의 MTU를 갖는다. 따라서 MSS 의 일반적인 값은 1469바이트다. MSS가 헤더를 포함하는 TCP 세그먼트의 최대 크기가 아니라, 세그먼트에 있는 애플리케이션 계층 데이터에 대한 최대 크기라는 점을 주의하라.

TCP 는 TCP 헤더와 클라이언트 데이터를 하나로 짝지어 TCP 세그먼트를 구성한다. 세그먼트는 네트워크 계층에 전달되며, 네트워크 계층 IP 데이터그램 안에 각각 캡슐화된다. TCP 가 상대에게서 세그먼트를 수신했을 때, 세그먼트의 데이터는 TCP 연결의 수신 버퍼에 위치한다. 애플리케이션은 이 버퍼로부터 데이터의 스트림을 읽는다. 연결의 양 끝은 각각 자신의 송신 버퍼와 수신 버퍼를 갖고 있다.

TCP 세그먼트 구조

TCP 세그먼트는 헤더 필드와 데이터 필드로 구성되어 있다. 데이터 필드는 애플리케이션 데이터의 일정량을 담는다. TCP 가 웹 문서의 이미지와 같은 큰 파일을 전송할 때, 일반적으로 MSS 크기로 파일을 분절한다. 그러나 많은 대화식 애플리케이션은 MSS 보다 작은 양의 데이터를 전송한다.

UDP 처럼 헤더는 상위 계층 애플리케이션으로부터 다중화와 역다중화를 하는 데 사용하는 출발지와 목적지 포트 번호(source and destination port number)를 포함한다. 또한 UDP 처럼 헤더는 체크섬 필드를 포함한다. 또한 다음과 같은 필드를 포함한다.

  • 32비트 순서 번호 필드(sequence number field) 와 32비트 확인응답 번호 필드(acknowledgement number field) 는 신뢰적인 데이터 전송 서비스 구현에서 TCP 송신자와 수신자에 의해 사용된다.
  • 16비트 수신 윈도(receive window) 필드는 흐름 제어에 사용된다. 이는 수신자가 받아들이려는 바이트의 크기를 나타내는 데 사용된다.
  • 4비트 헤더 길이 필드(header length field)는 32비트 워드 단위로 TCP 헤더의 길이를 나타낸다. TCP 헤더는 이전에 논의한 TCP 옵션 필드 때문에 가변적인 길이가 될 수 있다.
  • 선택적이고 가변적인 길이의 옵션 필드(Option field) 는 송신자와 수신자가 최대 세그먼트 크기(MSS)를 협상하거나 고속 네트워크에서 사용하기 위한 윈도 확장 요소로 이용된다.
  • 플래그 필드(flag field) 는 6비트를 포함한다. ACK 비트는 확인응답 필드에 있는 값이 유용함을 가리키는 데 사용된다. 즉, 이 세그먼트는 성공적으로 수신된 세그먼트에 대한 확인응답을 포함한다. RST, SYN, FIN 비트는 연결 설정과 해제에 사용된다. PSH 비트가 설정될 때, 이것은 수신자가 데이터를 상위 계층에 즉시 전달해야 한다는 것을 가리킨다. 마지막으로, URG 비트는 이 세그먼트에서 송신 측 상위 계층 개체가 ‘긴급’으로 표시하는 데이터임을 가리킨다. 이 긴급 데이터의 마지막 바이트의 위치는 16비트의 긴급 데이터 포인터 필드(urgent data pointer field)에 의해 가리켜진다. TCP 는 긴급 데이터가 존재할 때 수신 측 상위 계층 개체에게 통지해야 하고 긴급 데이터의 끝에 대한 포인터를 전달한다(PSH, URG 그리고 긴급 데이터에 대한 포인터는 실제 사용되지 않지만 완벽하게 하도록 이러한 필드를 언급한다)

순서 번호와 확인응답 번호

TCP 세그먼트 헤더에서 가장 중요한 필드 두 가지는 순서 번호 필드와 확인응답 번호 필드다. 이런 필드들은 TCP 의 신뢰적인 데이터 전송 서비스의 중대한 부분이다.

TCP는 데이터를 구조화되어 있지 않고 단지 순서대로 정렬되어 있는 바이트 스트림으로 본다. TCP의 순서 번호 사용은 순서 번호가 일련의 전송된 세그먼트에 대해서가 아니라, 전송된 바이트의 스트림에 대해서라는 관점을 반영한 것이다. 세그먼트에 대한 순서 번호는 세그먼트에 있는 첫 번째 바이트의 바이트 스트림 번호다.

호스트 A에서의 프로세스는 TCP 연결상에서 호스트 B의 프로세스로 데이터 스트림의 전송을 원한다고 가정하자. 호스트 A의 TCP 는 데이터 스트림의 각 바이트에 암시적으로 순서 번호를 지정한다. 데이터 스트림은 500,000 바이트로 구성된 파일이라 가정하자. 또한 MSS 는 1000 바이트이고 데이터 스트림의 첫 번째 바이트는 0으로 설정했다고 하자. TCP 는 데이터 스트림으로부터 500개의 세그먼트들을 구성한다. 첫 번째 세그먼트는 순서 번호 0, 두 번째 세그먼트는 순서 번호 1000, 세 번째 세그먼트는 순서 번호 2000과 같은 식으로 할당된다. 각각의 순서 번호는 적절한 TCP 세그먼트의 헤더 내부의 순서 번호 필드에 삽입된다.

확인응답 번호를 생각해보자. 이것은 순서 번호보다 약간 까다롭다. TCP는 호스트 A가 호스트 B로 데이터를 송신하는 동안에 호스트 B로부터 데이터를 수신하게 해주는(같은 TCP 연결의 부분처럼) 전이중 방식임을 상기하자. 호스트 B로부터 도착한 각 세그먼트는 B로부터 A로 들어온 데이터에 대한 순서 번호를 갖는다. 호스트 A가 자신에 세그먼트에 삽입하는 확인응답 번호는 호스트 A가 호스트 B로부터 기대하는 다음 바이트의 순서 번호다. TCP 는 스트림에서 첫 번째 잃어벌니 바이트까지의 바이트들까지만 확인응답하기 때문에, TCP는 누적 확인응답을 제공한다고 한다.

호스트 A는 세 번째 세그먼트를 두 번재 세그먼트가 수신되기 전에 수신했다. 즉, 세 번째 세그먼트는 순서가 틀리게 도착했다. 민감한 문제는 TCP 연결에서 순서가 바뀐 세그먼트를 수신할 때 호스트는 어떤 행동을 하는가에 대한 것이다. 흥미롭게도, TCP RFC 는 여기에 어떤 규칙도 부여하지 않았고 TCP 구현 개발자에게 맡기고 있다.

기본적으로 다음의 두 가지 선택이 있다.

  1. 수신자가 순서가 바뀐 세그먼트를 즉시 버린다.
  2. 수신자는 순서가 바뀐 데이터를 보유하고, 빈 공간에 잃어버린 데이터를 채우기 위해 기다린다.

확실히 후자가 네트워크 대역폭 관점에서는 효율적이며, 실제에서도 취하는 방법이다.

예제에서는 시작 순서 번호를 0으로 가정했으나, 실제로는 TCP 연결의 양쪽 모두 시작 순서 번호를 임의로 선택한다. 이것은 두 호스트 사이에 이미 종료된 연결로부터 아직 네트워크에 남아 있던 세그먼트가 같은 두 호스트 간의 나중 연결에서 유효한 세그먼트로 오인될 확률을 최소화한다.

텔넷: 순서 번호와 응답확인 번호 사례연구

RFC 854에서 정의된 텔넷은 원격 로그인을 위해 사용되는 유명한 애플리케이션 계층 프로토콜이다. 이것은 TCP 상에서 실행되며, 한 쌍의 호스트들 사이에서 동작하도록 설계되었다.

호스트 A가 호스트 B와 텔넷 세션을 시작한다고 가정하자. 호스트 A가 세션을 시작하므로 클라이언트이고, 호스트 B는 서버가 된다. 사용자가 입력한 각 문자는 원격 호스트에게 송신될 것이다. 원격 호스트는 각 문자의 복사본을 송신자에게 반송하여 텔넷 사용자의 화면에 표시되게 할 것이다. 이 ‘에코 백(echo back)’ 은 텔넷 사용자가 보는 문자가 이미 원격 사이트에 수신되고 처리되었음을 나타낸다. 그러므로 각 문자는 사용자가 키를 누르는 시간과 문자가 사용자의 모니터에 표시되는 시간 사이에서 네트워크를 두 번 횡단한다.

이제 사용자가 하나의 문자 ‘C’ 를 입력하고 커피를 마신다고 가정하자. 클라이언트와 서버 사이에서 송신되는 TCP 세그먼트를 조사하자. 초기 순서 번호가 클라이언트와 서버에 대해 각각 42와 79라고 가정한다. 세그먼트의 순서 번혼느 데이터 필드 안에 있는 첫 번째 바이트의 순서 번호이다. 그러므로 클라이언트에서 송신된 첫 번째 세그먼트는 순서 번호 42를 가질 것이다. 서버로부터 송신된 첫 번째 세그먼트는 순서 번호 79를 가질 것이다. 확인응답 번호는 호스트가 기다리는 데이터의 다음 바이트의 순서 번호이다. TCP 연결이 설정된 후에 어떤 데이터도 송신되기 전에 클라이언트는 바이트 79를 기다리고 있고, 서버는 바이트 42를 기다리고 있다.

3개의 세그먼트가 송신된다. 첫 번째 세그먼트는 클라이언트에서 서버로 송신된다. 그 세그먼트는 데이터 필드 안에 문자 ‘C’의 1바이트의 ASCII 표현을 포함한다. 이 첫 번째 세그먼트는 순서 번호 필드 안에 42를 갖는다. 또한 클라이언트가 아직 서버로부터 어떠한 데이터도 수신하지 못했으므로, 이 첫 번째 세그먼트는 그것의 확인응답 번호 필드에 79를 가질 것이다.

두 번째 세그먼트는 서버에서 클라이언트로 송신되는데, 다음과 같이 두 가지 목적을 갖는다.

  1. 수신하는 서버에게 데이터에 대한 확인응답을 제공한다. 확인응답 필드 안에 43을 넣음으로써, 서버는 클라이언트에게 바이트 42를 성공적으로 수신했고 앞으로 바이트 43을 기다린다는 것을 말해준다.
  2. 문자 C를 반대로 반향되도록 하는 것이다. 그러므로 두 번째 세그먼트는 그것의 데이터 필드에 ‘C’ 의 ASCII 표현을 한다.

이 두 번째 세그먼트는 이러한 TCP 연결의 서버-클라이언트 데이터 흐름의 최초 순서 번호인 순서 번호 79를 갖는다. 이것은 서버가 보내는 데이터의 맨 첫 번째 바이트다. 클라이언트/서버 데이터에 대한 확인응답은 서버와 클라이언트 간에 데이터를 운반하는 세그먼트 안에서 전달된다는 것에 주의한다. 이러한 확인응답은 서버-클라이언트 데이터 세그먼트상에서 피기백된다(piggybacked)고 말한다.

세 번째 세그먼트는 클라이언트에서 서버로 송신되는데, 그 목적은 서버로부터 수신한 데이터에 대한 확인응답을 하는 것이다. 이 세그먼트는 빈 데이터 필드를 갖는다. 즉, 확인응답은 어떤 클라이언트-서버 데이터와 함께 피기백되지 않는다. 세그먼트는 확인응답 번호 필드 안에 80을 갖는다. 클라이언트가 순서 번호 79의 바이트를 통해 바이트의 스트림을 수신했기 때문이다. 그리고 이제 앞으로 80으로 시작하는 바이트를 기다린다. 이 세그먼트가 데이터를 포함하지 않는데도 순서 번호를 갖는다는 것이 이상하게 보일 수도 있따. 그러나 TCP 가 순서 번호 필드를 갖고 있음에도 세그먼트 역시 어떤 순서 번호를 가져야 한다.

왕복 시간(RTT) 예측과 타임아웃

TCP가 손실 세그먼트를 발견하기 위해 타임아웃/재전송 메커니즘을 사용한다는 것을 알아보자.

왕복 시간 예측

TCP 가 송신자와 수신자 사이에 왕복 시간을 어떻게 예측하는지 생각해보자. SampleRTT라고 표시되는 세그먼트에 대한 RTT 샘플은 세그먼트가 송신된 시간(즉, IP에게 넘겨진 시간)으로부터 그 세그먼트에 대한 긍정응답이 도착한 시간까지의 시간 길이다. 모든 전송된 세그먼트에 대해 SampleRTT 를 측정하는 대신, 대부분의 TCP는 한 번에 하나의 SampleRTT 측정만을 시행한다. 즉, 어떤 시점에서 SampleRTT는 전송되었지만 현재까지 확인응답이 없는 세그먼트 중 하나에 대해서만 측정되며 이는 대략 왕복 시간마다 SampleRTT의 새로운 값을 얻게 한다. 또한 TCP는 재전송한 세그먼트에 대한 SampleRTT는 계산하지 않으며, 한 번 전송된 세그먼트에 대해서만 측정한다.

SampleRTT 값은 라우터에서의 혼잡과 종단 시스템에서의 부하 변화 때문에 세그먼트마다 다르다. 이러한 변동성 때문에 SampleRTT 값은 불규칙적이다. 대체로 RTT 를 추정하기 위해 SampleRTT 값의 평균값을 채택한다. TCP는 SampleRTT 값의 평균(EstimatedRTT) 을 유지한다. 긍정 확인응답을 수신하고 새로운 SampleRTT 를 획득하자마자 TCP는 다음 공식에 따라 EstimatedRTT 를 갱신한다.

EstimatedRTT=(1α)EstimatedRTT+αSampleRTTEstimatedRTT = (1-\alpha) * EstimatedRTT + \alpha * SampleRTT

EstimatedRTT 의 새로운 값은 EstimatedRTT 의 이전 값과 SampleRTT에 대한 새로운 값의 가중된 조합이다. 권장되는 α\alpha의 값은 0.125 이며, 이러한 경우에 위의 공식은 다음과 같다.

EstimatedRTT=0.875EstimatedRTT+0.125SampleRTTEstimatedRTT = 0.875 * EstimatedRTT + 0.125 * SampleRTT

EstimatedRTT 는 SampleRTT 값의 가중평균임을 유념하라. 이 가중평균은 예전 샘플보다 최근 샘플에 높은 가중치를 준다. 최신 샘플들이 네트워크상의 현재 혼잡을 더 잘 반영한다. 통계에서 이런 평균은 지수적 가중 이동 평균(exponential weighted moving average, EWMA) 이라고 부른다. 주어진 SampleRTT 의 가중치가 갱신 절차가 진행됨에 따라 빠르게 지수적으로 감소하므로 지수적이라는 용어가 쓰인다.

RTT의 예측 외에 RTT의 변화율을 측정하는 것도 매우 유용하다. RFC 6298은 RTT 변화율을 의미하는 DevRTT 를 SampleRTT 가 EstimatedRTT 로부터 얼마나 많이 벗어나는지에 대한 예측으로 정의한다.

DevRTT=(1β)DevRTT+βSampleRTTEstimatedRTTDevRTT = (1-\beta) * DevRTT + \beta * |SampleRTT - EstimatedRTT|

DevRTT 는 SampleRTT와 EstimatedRTT 값 차이의 EWMA 임을 유념하자. 만일 SampleRTT 값이 어떠한 변화도 없다면 DevRTT 는 작을 것이며, 그렇지 않다면 DevRTT 는 클 것이다. β\beta의 권장값은 0.25이다.

재전송 타임아웃 주기의 설정과 관리

주어진 EstimatedRTT 와 DevRTT 의 값에서, TCP 타임아웃 주기에는 어떤 값이 사용되어야 하는가? 분명히 주기는 EstimatedRTT 보다 크거나 같아야 한다. 그렇지 않다면 불필요한 재전송이 보내질 것이다. 그러나 타임아웃 주기는 EstimatedRTT 보다 너무 크면 안 된다. 너무 크면 세그먼트를 잃었을 때, TCP는 세그먼트의 즉각적인 재전송을 하지 않게 된다. 이 때문에 애플리케이션에서 확연한 데이터 전송 지연이 나타난다. 그러므로 타임아웃값은 EstimatedRTT 에 약간의 여윳값을 더한 값으로 설정하는 것이 바람직하다. SampleRTT 값에 많은 변동이 있을 때는 여윳값이 커야 하며, 변동이 작을 때에는 작아야 한다. 따라서 DevRTT 값이 역할을 하게 된다.

TimeoutInterval=EstimatedRTT+4DevRTTTimeoutInterval = EstimatedRTT + 4 * DevRTT

초기 TimeoutInterval의 값으로 1초를 권고한다. 또한 타임아웃이 발생할 때, TimeoutInterval 의 값은 두 배로 하여 조만간 확인응답할 후속 세그먼트에게 발생할 수 있는 조기 타임아웃을 피하도록 한다.

신뢰적인 데이터 전송

인터넷의 네트워크 계층(IP 서비스)은 비신뢰적이라고 앞에서 배웠다. 인터넷 프로토콜은 데이터그램 전달을 보장하지 않고, 데이터그램이 순서대로 전달된다는 것을 보장하지 않는다. 또한 데이터그램에 포함된 데이터의 무결성을 보장하지 않는다. IP 서비스에서 데이터그램은 라우터의 버퍼를 오버플로 상태로 만들 수 있으며, 목적지에 도달하지 않을 수도 있고, 데이터그램의 순서가 틀리게 도달할 수도 있다. 또한 데이터그램의 비트가 손상될 수도 있다. 트랜스포트 계층의 세그먼트가 IP 데이터그램에 의해 네트워크 상에서 운반되므로, 트랜스포트 계층의 세그먼트도 이런 문제를 겪을 수 있다.

TCP 는 IP 의 비신뢰적인 최선형 서비스에서 신뢰적인 데이터 전송 서비스(reliable data transfer service) 를 제공한다. TCP 의 신뢰적인 데이터 전송 서비스는 프로세스가 자신의 수신 버퍼로부터 읽은 데이터 스트림이 손상되지 않았으며 손실이나 중복이 없다는 것과 순서가 유지된다는 것을 보장한다. 즉, 바이트 스트림은 송신자가 전송한 것과 같은 바이트 스트림이다.

신뢰적인 데이터 전송에 대한 앞에서의 전개 과정에서, 전송되었지만 아직 확인응답을 받지 못한 각각의 세그먼트와 개별적인 타이머가 한 쌍이 되어 동작한다고 가정하는 것이 개념상 가장 쉬웠다. 이것이 이론상으로는 훌륭할지 몰라도, 타이머 관리는 상당한 오버헤드를 요구할 수도 있다. 따라서 전송되었지만 확인응답이 안 된 다수의 세그먼트들이 있다고 하더라도, 권장되는 TCP 타이머 관리 절차에서는 오직 단일 재전송 타이머를 사용한다.

TCP 가 어떻게 신뢰적인 데이터 전송을 제공하는지 2개의 점진적인 단계로 알아보자.

TCP 송신자를 매우 간소화하여 설명한 코드를 살펴보자. (p.218) 여기서 TCP 송신자의 데이터 전송/재전송에 관련된 세 가지 주요 이벤트가 있음을 볼 수 있는데, 그것은 상위 애플리케이션으로부터 수신된 데이터, 타이머 타임아웃, ACK 수신이다. 첫 번째 주요 이벤트 발생으로 TCP 는 애플리케이션으로부터 데이터를 받고, 세그먼트로 이 데이터를 캡슐화하고, IP 에게 이 세그먼트를 넘긴다. 또한 타이머가 이미 다른 세그먼트에 대해 실행 중이 아니면, TCP 는 이 세그먼트를 IP 로 넘길 때 타이머를 시작한다. 이 타이머에 대한 만료 주기는 TimeoutInterval 이다.

두 번째 주요 이벤트는 타임아웃이다. TCP는 타임아웃 이벤트에 대해 타임아웃을 일으킨 세그먼트를 재전송하여 응답한다. 그리고 TCP 의 타이머를 다시 시작한다.

세 번째 주요 이벤트는 수신자로부터의 수신 확인응답 세그먼트(ACK) 수신이다. TCP 는 변수 SendBase 와 ACK 값 y를 비교한다. TCP 상태 변수 SendBase 는 수신 확인응답이 확인되지 않은 가장 오래된 바이트의 순서번호다. TCP 는 누적 확인응답을 사용하고, y는 y바이트 이전의 모든 바이트의 수신을 확인한다. 만일 y > SendBase 이면, ACK 는 이전에 확인응답 안 된 하나 이상의 세그먼트들을 확인해준다. 따라서 송신자는 자신의 SendBase 변수를 갱신한다. 또한 아직 확인응답 안 된 세그먼트들이 존재한다면 타이머를 다시 시작한다.

몇 가지 흥미로운 시나리오

이 간단한 버전에는 많은 미묘한 부분들이 있다.

호스트 A가 호스트 B에게 하나의 세그먼트를 보내는 시나리오를 가정하자. 이 세그먼트가 순서 번호 92와 8바이트의 데이터를 포함한다고 가정한다. 호스트 A가 이 세그먼트를 전송한 후에 호스트 A는 호스트 B로부터 긍정 확인응답 번호 100을 가진 세그먼트를 기다린다. A로부터의 세그먼트가 B측에서 수신되었음에도 B로부터 A로의 긍정 확인응답이 손실된다면, 이런 경우에 타임아웃이 일어나고 호스트 A는 같은 세그먼트를 B에게 재전송한다. 물론, 호스트 B가 재전송 세그먼트를 수신했을 때 호스트 B는 세그먼트가 포함하고 있는 순서 번호로부터 이미 수신된 데이터임을 알 수 있다. 이와 같이 호스트 B의 TCP는 재송신된 세그먼트의 바이트를 버릴 것이다.

두 번째 시나리오에서는 호스트 A가 연속해서 두 세그먼트를 전송한다. 첫 번째 세그먼트는 순서 번호 92와 8바이트의 데이터를 가지고 있고, 두 번째 세그먼트는 순서 번호 100과 20 바이트의 데이터를 가지고 있다. 두 세그먼트 모두 호스트 B에 무사히 도착한다고 가정하면, B는 각각의 세그먼트에 대해 2개의 개별적인 긍정 확인응답을 전달한다. 이제 호스트 A에서 타임아웃 이전에 어떠한 긍정 확인응답도 수신하지 못한다고 가정하자. 타임아웃 이벤트가 발생하면, 호스트 A는 순서 번호 92로 첫 번째 세그먼트를 재전송하고 타이머를 다시 시작한다. 새로운 타임아웃 이전에 두 번째 세그먼트에 대한 ACK 가 도착하지 않는 한, 두 번째 세그먼트는 재전송을 하지 않을 것이다.

마지막 시나리오는 두 번째 예처럼 호스트 A가 똑같이 2개의 세그먼트를 전송한다고 가정하는 것이다. 첫 번째 세그먼트의 긍정 확인응답이 네트워크에서 분실되었지만, 첫 번째 세그먼트의 타임아웃 전에 호스트 A가 긍정 확인응답 120의 긍정 확인응답을 수신하면, 호스트 A는 호스트 B가 119바이트까지 모든 데이터를 수신했음을 알게 된다. 그러므로 호스트 A는 두 세그먼트 중 어느 것도 재전송하지 않는다.

타임아웃 주기의 두 배로 설정

대부분의 TCP 구현에서 사용하는 몇 가지 수정사항을 논의해보자. 첫 번째는 타임아웃 주기의 길이에 대한 것이다. 이 수정에서는 타임아웃이 발생할 때마다 TCP 는 아직 확인응답이 안 된 가장 작은 순서 번홀르 가진 세그먼트를 재전송한다. 그러나 TCP 는 재전송 때마다 마지막 EstimatedRTT 와 DevRTT 로부터 타임아웃값을 가져오는 것이 아니라 타임아웃 주기를 이전 값의 두 배로 설정한다. 이 수정은 제한된 형태의 혼잡 제어를 제공한다. 타이머 종료는 주로 네트워크에서의 혼잡에 의해 발생한다. 즉, 출발지와 목적지 사이의 경로에서 하나 이상의 라우터 버퍼에 도착한 많은 패킷은 패킷의 손실이나 오랜 큐 대기의 원인이 된다. 혼잡할 때 출발지에서 지속해서 패킷의 재전송을 고집하면 그 혼잡은 더욱 악화될 것이다. 대신에 TCP는 송신자가 더 긴 간격으로 재전송하도록 한다.

빠른 재전송

타임아웃이 유발하는 재전송의 한 가지 문제는 타임아웃의 주기가 때때로 비교적 길다는 것이다. 세그먼트를 잃었을 때, 긴 타임아웃 주기는 잃어벌니 패킷을 다시 보내기 전에 송신자를 오랫동안 기다리게 해서 종단 간의 지연을 증가시킨다. 다행히도, 송신자는 종종 소위 중복 ACK 에 의한 타임아웃이 일어나기 전에 패킷 손실을 발견한다. 중복 ACK 는 송신자가 이미 이전에 받은 확인응답에 대한 재확인응답 세그먼트 ACK 다. 중복 ACK 에 대한 송신자의 반응을 이해하기 위해, 먼저 수신자가 중복된 ACK 를 보내는 이유를 알아야 한다.

이벤트 TCP 수신자 동작
기다리는 순서 번호를 가진 ‘순서가 맞는’ 세그먼트의 도착. 기다리는 순서 번호까지의 모든 데이터는 이미 확인응답됨. 지연된 ACK. 또 다른 ‘순서가 맞는’ 세그먼트의 도착을 위해 500ms 까지 기다린다. 만약 다음 ‘순서에 맞는’ 세그먼트가 이 기간에 도착하지 않으면, 그냥 ACK 를 보낸다.
기다리는 순서 번호를 가진 ‘순서가 맞는’ 세그먼트의 도착. ACK 전송을 기다리는 다른 하나의 ‘순서에 맞는’ 세그먼트가 있음. 2개의 ‘순서가 맞는’ 세그먼트들을 ACK 하기 위해, 하나의 누적된 ACK 를 즉시 보낸다.
기다리는 것보다 높은 순서 번호를 가진 ‘순서가 바뀐’ 세그먼트의 도착. 격차가 발견됨. 순서 번호가 다음의 기다리는 바이트(즉, 격차의 최솟값)을 나타내는 중복 ACK 를 즉시 보낸다.
수신 데이터에서 격차를 부분적으로 또는 모두 채우는 세그먼트의 도착 그 세그먼트가 격차의 최솟값에서 시작한다고 하면, 즉시 ACK를 보낸다.
TCP 는 부정 확인응답을 사용하지 않으므로, 수신자는 송신자에게 부정 확인응답을 보낼 수 없다. 대신에, 수신자는 마지막으로 수신된 순차적인 바이트를 갖는 데이터를 그냥 다시 확인응답을 한다.

송신자는 종종 많은 양의 세그먼트를 연속적으로 보낼 수 있으므로, 만약 하나의 세그먼트를 잃어버린다면 많은 연속적인 중복 ACK 가 존재할 수 있다. 만약 TCP 송신자가 같은 데이터에 대해 3개의 중복 확인응답을 수신한다면, 이것은 ACK 된 세그먼트의 다음 3개의 세그먼트가 분실되었음을 의미한다. 3개의 중복 ACK를 수신할 때, TCP 는 세그먼트의 타이머가 만료되기 이전에 손실 세그먼트를 재전송하는 빠른 재전송(fast retransmit) 을 한다.

GBN인가 SR인가?

TCP가 GBN 프로토콜인가, 아니면 SR 프로토콜인가? TCP 확인응답은 누적되고 올바르게 수신되지만, 순서가 잘못된 세그먼트는 수신자가 개별적으로 ACK를 받지 않는다. 결론적으로 TCP 송신자는 전송했지만 확인응답 안 된 바이트의 가장 작은 순서 번호를 유지해야 ㅎ나다. 이런 관점에서 TCP 는 GBN 프로토콜과 매우 비슷하게 보이지만, 몇 가지 차이점이 있다. 많은 TCP 구현에서는 올바르게 수신되었지만 순서가 바뀐 세그먼트들을 버퍼링한다. 또한 TCP 는 세그먼트 n에 대한 타임아웃 전에 세그먼트 n+1 에 대한 긍정 확인응답이 도착한다면 세그먼트를 재전송하지 않는다. 선택적 재전송과 결합했을 경우, TCP 는 원래의 SR 프로토콜과 매우 유사하다. 따라서 TCP 의 오류 복구 메커니즘은 GBN 과 SR 프로토콜의 혼합으로 분류하는 것이 적당하다.

흐름 제어

TCP 연결의 각 종단에서 호스트들은 연결에 대한 개별 수신 버퍼를 설정한다고 배웠다. TCP 연결이 순서대로 올바르게 바이트를 수신할 때 TCP 는 데이터를 수신 버퍼에 저장한다. 해당 애플리케이션 프로세스는 버퍼에서 데이터를 읽지만, 데이터가 도달한 시점에 읽을 필요는 없다. 애플리케이션이 데이터를 읽는 속도가 비교적 느리다면, 점점 더 많은 데이터를 빠르게 전송함으로써 연결의 수신 버퍼에 아주 쉽게 오버플로를 발생시킨다.

이처럼 TCP 는 송신자가 수신자의 버퍼를 오버플로시키는 것을 방지하기 위해 애플리케이션에게 흐름 제어 서비스(flow-control service)를 제공한다. 이와 같이 흐름 제어는 속도를 일치시키는 서비스이다. 수신하는 애플리케이션이 읽는 속도와 송신자가 전송하는 속도를 같게 한다. TCP 송신자는 IP 네트워크에서 혼잡 때문에 억제될 수도 있다. 송신자 제어의 이 형태는 혼잡 제어 (congestion control) 로 알려져 있다. 흐름 제어와 혼잡 제어에서 수행되는 동작들이 비슷하지만, 흐름 제어와 혼잡 제어는 명백히 각기 다른 목적으로 수행된다.

TCP 는 송신자가 수신 윈도라는 변수를 유지하여 흐름 제어를 제공한다. 수신 윈도는 수신 측에서 가용한 버퍼 공간이 얼마나 되는지를 송신자에게 알려주는 데 사용된다. TCP 는 full-duplex 이므로 연결의 각 측의 송신자는 별개의 수신 윈도를 유지한다. TCP 연결상에서 호스트 A가 호스트 B에게 큰 파일을 전송한다고 가정할 때, 호스트 B는 이 연결에 수신 버퍼를 할당한다. 이때 할당된 수신 버퍼의 크기를 RcvBuffer 라고 명명한다. 호스트 B의 애플리케이션 프로세스는 버퍼로부터 데이터를 읽으며 다음과 같은 변수를 정의한다.

  • LastByteRead: 호스트 B의 애플리케이션 프로세스에 의해 버퍼로부터 읽힌 데이터 스트림의 마지막 바이트의 번호
  • LastByteRcvd: 호스트 B에서 네트워크로부터 도착하여 수신 버퍼에 저장된 데이터 스트림의 마지막 바이트의 번호

TCP 는 할당된 버퍼의 오버플로를 허용하지 않으므로 다음 수식이 가능하다.

LastByteRcvdLastByteRead<=RcvBufferLastByteRcvd - LastByteRead <= RcvBuffer

rwnd 로 명명된 수신 윈도는 버퍼의 여유 공간으로 설정된다.

rwnd=RcvBuffer[LastByteRcvdLastByteRead]rwnd=RcvBuffer-[LastByteRcvd - LastByteRead]

시간에 따라 여유 공간은 변하므로 rwnd 는 동적이다.

연결은 흐름 제어 서비스를 제공하기 위해 변수 rwnd 를 어떻게 사용하는가? 호스트 B는 호스트 B가 호스트 A에게 전송하는 모든 세그먼트의 윈도 필드에 현재의 rwnd 값을 설정함으로써 연결 버퍼에 얼마만큼의 여유 공간이 있는지를 호스트 A에게 알려준다. 초기에는 호스트 B가 rwnd = RcvBuffer 로 설정한다. 반면에, 호스트 A는 명백한 의미가 있는 두 변수 LastByteSent 와 LastByAcked 를 유지한다. 이 두 변수의 차이는 호스트 A가 이 연결에 전송 확인응답이 안 된 데이터의 양이다. rwnd 의 값보다 작은 확인응답 안 된 데이터의 양을 유지함으로써 호스트 A는 호스트 B의 수신 버퍼에 오버플로가 발생하지 않는다는 것을 확신한다. 이와 같이 호스트 A는 연결된 동안 다음 내용을 보장한다.

LastBySentLastByteAcked<=rwndLastBySent - LastByteAcked <= rwnd

이 방식에는 사소한 기술적 문제가 있다. 호스트 B의 수신 버퍼는 rwnd = 0 으로서 가득 찼다고 가정하자. 호스트 A에게 wnd = 0 이라고 알린 후에 호스트 B는 호스트 A에게 전송할 게 없다고 가정하자. 호스트 B에서의 애플리케이션 프로세스가 버퍼를 비우더라도, TCP 는 호스트 A에게 새로운 rwnd 로 새로운 세그먼트를 전송하지 않는다. 즉, TCP 는 전송할 데이터가 있거나 전송해야 할 확인응답을 가진 경우에만 호스트 A에게 세그먼트를 전송할 것이기 때문에, 호스트 A는 호스트 B의 수신 버퍼에 약간의 공간이 있다는 것을 모른다. 호스트 A는 차단되고 더는 데이터를 전송할 수 없다. 이러한 문제를 해결하기 위해 TCP 명세서는 호스트 A가 호스트 B의 수신 윈도가 0일 때, 1바이트 데이터로 세그먼트를 계속해서 전송하도록 요구한다. 이 세그먼트들은 수신자에 의해 긍정 확인응답될 것이고, 결과적으로 버퍼는 비워지고 긍정 확인응답은 0이 아닌 rwnd 값을 포함한다.

UDP 는 흐름 제어를 제공하지 않는다. 호스트 A의 한 프로세스로부터 호스트 B의 프로세스에게 연속적인 UDP 세그먼트를 전송하는 경우를 생각해보자. 전형적인 UDP 구현의 경우, UDP 는 댕으하는 소켓의 앞에 위치한 유한 크기 버퍼에 세그먼트를 쌓을 것이다. 프로세스는 버퍼로부터 한 번에 하나의 전체 세그먼트를 읽는다. 프로세스가 버퍼로부터 매우 빠르게 세그먼트를 읽지 못한다면, 버퍼는 오버플로될 것이고 세그먼트를 잃어버릴 것이다.

TCP 연결 관리

하나의 호스트(클라이언트)에서 운영되는 프로세스가 다른 호스트(서버) 안의 또 다른 프로세스와 연결을 시작하길 원한다고 가정하자. 먼저 클라이언트 애플리케이션 프로세스는 서버에 있는 프로세스와 연결 설정하기를 원한다는 것을 클라이언트 TCP 에게 알린다. 그러면 클라이언트 안의 TCP 는 다음과 같은 방법으로 TCP 를 이용해 서버와 TCP 연결 설정을 시작한다.

  1. 먼저 클라이언트 측 TCP 는 서버 TCP 에게 특별한 TCP 세그먼트를 송신한다. 이는 애플리케이션 계층 데이터를 포함하지 않는다. 그러나 세그먼트의 헤더에 SYN 비트라고 불리는 하나의 플래그 비트를 1로 설정한다. 이 세그먼트를 SYN 세그먼트라고 부른다. 추가로 클라이언트는 최초의 순서 번호를 임의로 선택하고, 최초의 TCP SYN 세그먼트의 순서 번호 필드에 이 번호를 넣는다. 이 세그먼트는 IP 데이터그램 안에 캡슐화되고 서버로 송신된다. (특정 보안 공격을 피하고자 순서 번호의 선택을 적절히 임의 추출하는 것에 유의해야 한다)
  2. TCP SYN 세그먼트를 포함하는 IP 데이터그램이 서버 호스트에 도착하면, 서버는 데이터그램으로부터 TCP SYN 세그먼트를 추출한다. 그리고 연결에 TCP 버퍼와 변수를 할당한다. 그리고 클라이언트 TCP로 연결 승인 세그먼트를 송신한다. 이 세그먼트도 애플리케이션 계층 데이터를 포함하지 않는다. 그러나 세그먼트 헤더 안에 3개의 중요한 정보를 포함한다. 첫째, SYN 비트는 1로 설정된다. 둘째, TCP 세그먼트 헤더의 확인응답 필드는 client_isn+1로 설정된다. 마지막으로, 서버는 최초의 순서 번호(server_isn)를 선택하고, TCP 세그먼트 헤더의 순서 번호 필드에 이 값을 넣는다. 이 세그먼트는 SYNACK 세그먼트로 불린다.
  3. 연결 승인 세그먼트를 수신하면, 클라이언트는 연결에 버퍼와 변수를 할당한다. 그 다음에 클라이언트 호스트는 서버로 또 다른 세그먼트를 송신한다. 이 마지막 세그먼트가 서버의 연결 승인 세그먼트를 확인하는 것이다. 연결이 설정되었으므로 SYN 비트는 0으로 설정된다. 여기서는 클라이언트에서 서버로의 데이털르 세그먼트 페이로드에서 운반할 수 있다.

위의 세 단계가 완료되면 클라이언트와 서버 호스트들은 각각 서로에게 데이터를 포함하는 세그먼트를 보낼 수 있다. 이 각각의 다음의 세그먼트에서 SYN 비트는 0으로 설정된다.

TCP 연결에 참여하는 두 프로세스 중 하나가 연결을 끝낼 수 있다. 연결이 끝날 때, 호스트의 자원은 회수된다.

TCP 연결이 존재하는 동안 각 호스트에서 동작하는 TCP 프로토콜은 다양한 TCP 상태를 두루 전이한다.

### SYN 플러드 공격
이 공격에서 공격자들은 핸드셰이크의 세 번째 단계를 완료하지 않은 상태에서 무수한 TCP SYN 세그먼트를 보낸다. 이 TCP SYN 세그먼트의 대홍수로 서버의 연결 자원이 반쪽 연결에 할당되어 서버의 연결 자원이 소진됨에 따라 합법적인 클라이언트들이 서비스 거부가 된다. 현재 대부분의 운영체제에는 이를 방어하기 위한 SYN 쿠키라는 방어책이 있다.

- 서버가 SYN 세그먼트를 받을 때, 그 세그먼트가 정당한 사용자로부터 온 것인지 SYN 플러드 공격의 한 부분인지 알 수 없다. 따라서 서버는 이 SYN 에 대해 반만 열린 TCP 연결을 만들지 않고, 대신 서버는 오직 자신만 아는 비밀번호뿐만 아니라 SYN 세그먼트의 출발지 목적지 IP 주소들과 포트 번호들의 복잡한 해시함수로 초기 TCP 순서 번호를 만든다. 이를 쿠키라고 부른다. 그러고 나서 서버는 이 특별한 초기 순서 번호를 가진 SYNACK 패킷을 보낸다. 중요한 것은 서버가 SYN 에 관련된 쿠키나 어떠한 다른 상태 정보를 기억하지 않는다는 것이다.
- 합법적인 클라이언트는 ACK 세그먼트를 회신한다. 이 ACK 를 받은 서버는 ACK 가 이전에 보낸 일부 SYN에 관한 것인지 확인해야 한다. 이때 쿠키를 가지고 확인한다. 합법적인 ACK 의 확인응답 필드에 있는 값은 SYNACK 에 있는 순서 번호에 1을 더한 것과 같다. 그렇다면 서버는 SYNACK 세그먼트에 있는 출발지와 목적지 IP 주소와 포트 번호, 비밀번호를 사용해서 동일한 해시 함수를 실행할 것이다. 만약 함수의 결과에 1을 더한 것이 클라이언트의 SYNACK에 있는 확인응답 번호와 같다면, 서버는 ACK 가 초기 SYN 세그먼트에 관련된 것이고, 올바른 것이라고 결론짓는다.
- 반면에 클라이언트가 ACK 세그먼트를 회신하지 않으면, 서버가 처음의 가짜 SYN에 대해 어떠한 자원도 할당하지 않았으므로 처음의 SYN은 서버에 해를 끼치지 못한다.